한 문장 요약
규격서의 표 한 줄 = 구조체 멤버 하나.
고민할 건 딱 네 가지다 — 어떤 타입? P 가 M/O/C 중 무엇? 길이는 고정인가 가변인가?
단, C 가 여러 개이면 먼저 “동시에 올 수 있나”를 묻는다 — 아니라면 tagged union (2-9).
그리고 다 정한 뒤에 마지막으로 멤버 순서 — 정렬 큰 것부터 (4장).
0. 코드 치기 전에 만드는 표 세 개
빈 파일 앞에서 막막한 이유는 뭘 만들지가 안 정해졌기 때문이다.
표부터 만들면 구조체는 기계적으로 나온다.
표 ①: 입력
필드
타입
P
조건 (P = C 일 때)
범위·길이
설명
nf_instance_id
string
M
—
36 (UUID)
NF 식별자
nf_type
enum
M
—
AMF/SMF/UPF…
NF 종류
heart_beat_timer
integer
O
—
1~3600
초 단위
ipv4_addresses
string[]
O
—
최대 8개, 각 15자
IPv4 목록
upf_info
object
C
nf_type == UPF
—
UPF 전용 정보
표 ②: 출력
필드
타입
M/O
설명
result_code
integer
M
201/400/500
location
string
O
생성된 리소스 URI
표 ③: 에러
상황
리턴코드
응답
필수 필드 누락
RET_INVALID_ARG
400 Bad Request
UUID 형식 위반
RET_BAD_FORMAT
400
이미 등록됨
RET_FAIL
409 Conflict
DB 오류
RET_IO_ERROR
500
P(Presence) 열이 제일 중요하다 — M / O / C
M(Mandatory) = 없으면 에러. O(Optional) = 없어도 정상.
C(Conditional) = 조건이 참이면 M, 거짓이면 없음. 조건 문장은 조건 열에 따로 옮겨 적는다.
이 구분을 안 하고 코드를 짜면, “보낸 적 없는 필드가 0으로 들어와서” 생기는
원인을 알 수 없는 버그가 나온다. (아래 2장에서 해결 — O 는 2-2~2-5, C 는 2-6~2-10)
1. 타입 매핑 — 규격서 타입을 C 타입으로
flowchart LR
subgraph SPEC["규격서"]
S1["integer"]
S2["string"]
S3["enumeration"]
S4["boolean"]
S5["array of X"]
S6["object"]
S7["bit string"]
end
subgraph C["C 타입"]
C1["int32_t / uint16_t<br/>★ 범위 보고 결정"]
C2["char[N+1]<br/>★ +1 은 널 종단"]
C3["typedef enum"]
C4["uint8_t (0/1)"]
C5["X arr[MAX];<br/>uint16_t cnt;"]
C6["중첩 struct"]
C7["uint32_t flags"]
end
S1-->C1
S2-->C2
S3-->C3
S4-->C4
S5-->C5
S6-->C6
S7-->C7
1-1. 정수 — 크기를 범위 보고 고른다
규격서 범위
C 타입
크기
0 ~ 255
uint8_t
1
0 ~ 65535
uint16_t
2
-32768 ~ 32767
int16_t
2
0 ~ 약 42억
uint32_t
4
그 이상 / 시각
int64_t, uint64_t
8
int 를 기본값으로 쓰지 말 것
int 의 크기는 플랫폼마다 다를 수 있다. 네트워크로 나가거나 파일에 저장되는
구조체에는 반드시 <stdint.h> 의 고정 크기 타입을 쓴다.
로컬 계산용 변수에만 int 를 쓴다.
“이 값은 1부터 시작하니까 0은 없음으로 쓰자” 라고 했는데
나중에 규격 개정으로 0이 유효값이 되면 전부 뒤집어야 한다.
애매하면 has_ 플래그를 쓰는 게 안전하다.
2-5. 세 방식 비교 (Optional 표현)
has_ 플래그
비트마스크
센티널
가독성
★★★
★★
★
메모리
필드당 1바이트
전체 4바이트
0
필드 추가
쉬움
비트 상수 추가
—
실수 위험
낮음 (매크로로 묶으면)
중간
높음
권장
기본으로 이것
Optional 15개 이상
문자열/포인터만
2-6. C (Conditional) — 규격서 표에는 M/O 말고 C 가 있다
3GPP 같은 규격서의 필드 표에는 P 열(Presence)이 있고, 값이 세 가지다.
P
뜻
코드 표현
validate 가 하는 일
M
항상 있어야 함
플래그 없음
없으면 에러
O
있어도, 없어도 됨
has_
있을 때만 범위 검사
C
조건이 참이면 M, 거짓이면 없음
has_ + 조건식
조건 참인데 없으면 에러 / 조건 거짓인데 있으면 정책대로
C 는 표만 봐서는 안 되고 설명 열의 조건 문장을 읽어야 한다.
(“nfType 이 SMF 이면 present”, “notifEnable 이 true 이면 M”)
그리고 이 조건 문장이 어떤 모양이냐가 코드 구조를 정한다 — 아래 2-8.
C 를 O 처럼 처리하면 검사가 통째로 빠진다
O 는 “없어도 정상”이라 validate 가 아무것도 안 한다. C 는 “어떤 경우엔 없으면 안 된다.”
표에서 C 를 O 로 옮겨 적으면, 필수여야 할 필드가 빠진 요청이 정상으로 통과한다.
0장의 표 ①에 조건 열을 하나 더 만들어 두는 이유다.
2-7. C 가 하나뿐일 때 — has_ + 조건 검사
구조체는 O 와 똑같다. 달라지는 건 validate 뿐이다.
① 규격서의 조건 문장을 표로 먼저 옮긴다 (코드 치기 전)
필드
조건
조건 참일 때
조건 거짓일 때
notif_uri
notif_enable == 1
필수
없어야 정상 (있으면 → 정책)
② 그 표가 그대로 코드가 된다
/* 조건식은 함수로 뽑는다 — validate 본문이 조건 문장으로 어지러워지지 않게 */static int cond_notif_uri_required(const sub_req_t *r){ return r->notif_enable != 0;}/* validate 안에서 */if (cond_notif_uri_required(r)) { if (!r->has_notif_uri) { /* ★ 조건 참인데 없다 → 에러 */ LOG_ERR("notif_uri required when notif_enable=1 (%s/%d)", __func__, __LINE__); return RET_INVALID_ARG; }} else if (r->has_notif_uri) { /* 조건 거짓인데 왔다 → 정책 결정 */ LOG_WRN("notif_uri ignored: notif_enable=0 (%s/%d)", __func__, __LINE__);}
"조건 거짓인데 왔다" 는 규격서를 확인하고 한 번만 정한다
엄격하게 RET_INVALID_ARG 로 거절할지, 관대하게 로그만 남기고 무시할지는 규격과 상대 구현에 따라 다르다.
정해 두지 않으면 함수마다 다르게 처리하게 된다. validate 한 곳에서만 결정한다.
2-8. C 가 여러 개일 때 — 조건 문장의 모양을 먼저 분류한다
C 가 여러 개라고 무조건 union 이 아니다. 함께 존재할 수 있는가로 갈린다.
flowchart TD
Q1{"C 필드들이 <b>동시에</b><br/>존재할 수 있나?"}
Q1 -->|"예 — 서로 무관"| P1["① 독립 조건<br/>각자 has_ + 각자 조건식<br/><b>union ✗</b>"]
Q1 -->|"예 — 같이 오거나 같이 없음"| P3["③ 묶음<br/>하위 struct 로 감싸고 has_ 하나"]
Q1 -->|"예 — 하나 이상이면 됨"| P4["④ 최소 하나<br/>각자 has_ +<br/>validate 에서 합계 ≥ 1"]
Q1 -->|"아니오 — 정확히 하나만"| Q2{"어느 것인지 알려 주는<br/>필드(판별자)가 있나?"}
Q2 -->|"있다 — 예: nf_type"| P2["② tagged union<br/>기존 enum 을 태그로 재사용"]
Q2 -->|"없다"| P2b["② tagged union<br/>kind enum 을 새로 추가"]
union 은 멤버들이 같은 메모리를 겹쳐 쓴다. 하나를 쓰면 다른 하나가 지워진다.
④ 의 ipv4 / ipv6 처럼 둘 다 올 수 있는데 union 으로 만들면,
둘째를 파싱하는 순간 첫째가 조용히 망가진다. 컴파일도 되고 에러도 안 난다.
union 은 “정확히 하나만 존재한다”고 규격이 보장할 때만.
2-9. 배타 C → tagged union (판별자 + union)
규칙은 하나다. “지금 어느 멤버가 유효한가”를 알려주는 태그(판별자)를 union 과 함께 둔다.
태그 없는 union 은 읽는 쪽이 어느 멤버를 봐야 하는지 알 방법이 없다.
규격서 표 (단순화한 예)
필드
타입
P
조건
nf_type
enum
M
AMF / SMF / UPF …
amf_info
object
C
nf_type == AMF 일 때
smf_info
object
C
nf_type == SMF 일 때
upf_info
object
C
nf_type == UPF 일 때
세 C 는 nf_type 이 하나로 정하므로 동시에 존재할 수 없다 → 배타 → union.
그리고 판별자가 이미 M 필드(nf_type)로 있다 → 새 태그를 만들 필요 없이 그걸 쓴다.
/* ── 멤버마다 자기 구조체로 감싼다 ─────────────────────── */typedef struct { /* AMF: 값이 한 개씩 */ uint16_t set_id; uint8_t region_id;} amf_info_t;typedef struct { /* SMF: 값이 한 개씩 */ uint16_t tac; char dnn[DNN_SIZE];} smf_info_t;typedef struct { /* UPF: ★ 여러 개 — 배열과 개수를 이 안에 가둔다 */ uint8_t has_fqdn; uint8_t sst_cnt; uint8_t sst[MAX_UPF_SST]; char fqdn[FQDN_SIZE];} upf_info_t;/* ── 본 구조체 ──────────────────────────────────────────── */typedef struct { nf_type_t nf_type; /* ★ 태그: 아래 union 중 무엇이 유효한지 */ union { /* ★ nf_type 이 가리키는 것 하나만 유효 */ amf_info_t amf; smf_info_t smf; upf_info_t upf; } info; char nf_id[NF_ID_SIZE]; /* 정렬 1 → 맨 끝 (4장) */} nf_profile_t;
"C 가 한 개씩 / 여러 개" 가 섞여 있으면 구조체로 감싸면 된다
upf_info_t 는 배열(여러 개)을 갖고, amf_info_t 는 단일 값만 갖는다.
이 차이를 각자의 구조체 안에 가두면 union 입장에서는 전부 “구조체 하나”라서 똑같이 다룰 수 있다.
배열의 cnt 도 그 구조체 안에 넣는다. union 밖에 두면 태그가 바뀔 때 엉뚱한 개수를 보게 된다.
멤버가 단일 값 하나뿐이어도 (예: uint16_t 하나) 구조체로 감싸 두면 나중에 필드가 늘어날 때 union 을 안 건드린다.
읽기와 쓰기는 항상 태그를 거친다
/* 읽기: 태그가 맞을 때만 포인터를 내준다. 틀리면 NULL — 쓰레기를 읽을 기회를 아예 안 준다 */const upf_info_t *nf_profile_get_upf(const nf_profile_t *p){ if (p == NULL || p->nf_type != NF_TYPE_UPF) return NULL; return &p->info.upf;}/* 쓰기: ① 태그 ② union 전체를 0 으로 ③ 그 멤버만 채운다 */int nf_profile_set_upf(nf_profile_t *p, const upf_info_t *src){ CHK_PTR(p); CHK_PTR(src); p->nf_type = NF_TYPE_UPF; memset(&p->info, 0x00, sizeof(p->info)); /* ★ 이전 멤버의 잔재 제거 */ p->info.upf = *src; return RET_OK;}
validate 는 유효한 멤버 하나만 본다
switch (p->nf_type) {case NF_TYPE_AMF: /* p->info.amf 만 검사 */ break;case NF_TYPE_SMF: if (p->info.smf.dnn[0] == '\0') { LOG_ERR("smf dnn missing (%s/%d)", __func__, __LINE__); return RET_INVALID_ARG; } break;case NF_TYPE_UPF: if (p->info.upf.sst_cnt == 0 || p->info.upf.sst_cnt > MAX_UPF_SST) { LOG_ERR("upf sst_cnt(%u) invalid (%s/%d)", p->info.upf.sst_cnt, __func__, __LINE__); return RET_INVALID_ARG; } break;default: /* PCF 등 info 가 없는 NF — 조건에 해당 없음 */ break;}
"조건은 참인데 xxx_info 가 안 왔다" 는 파싱 단계에서 잡는다
tagged union 구조체 안에는 “안 왔다”를 나타낼 자리가 없다. 태그가 UPF 이면 info.upf 는 늘 “있는” 것이다.
그러므로 JSON이나 바이트를 읽는 함수가 nf_type == UPF 인데 upfInfo 가 없으면
그 자리에서 RET_INVALID_ARG 를 내고 구조체를 채우지 않는다.
조건이 참일 때도 그 멤버가 있을 수도 없을 수도 있다면uint8_t has_info; 를 태그 옆에 하나 더 둔다.
판별자가 따로 없는 배타 C — kind enum 을 새로 만든다
/* 규격: "주소는 ipv4 또는 ipv6 중 정확히 하나" — 이걸 알려 주는 필드가 표에 없다 */typedef enum { ADDR_KIND_NONE = 0, /* ★ 0 = 아직 안 채움 (memset 과 맞아떨어진다) */ ADDR_KIND_V4, ADDR_KIND_V6, ADDR_KIND_END} addr_kind_t;typedef struct { addr_kind_t kind; /* ★ 태그 */ union { char v4[IPV4_STR_SIZE]; char v6[IPV6_STR_SIZE]; } u;} addr_t;
2-10. tagged union 규칙 6개
#
규칙
어기면
1
태그가 항상 짝으로 있다. 태그 없는 union 은 만들지 않는다
어느 멤버가 유효한지 알 수 없다
2
읽기 전에 태그부터 확인 (switch (tag) 또는 getter)
다른 멤버의 바이트를 엉뚱한 타입으로 해석한 쓰레기 값
3
쓸 때는 태그 설정 → memset(&u, 0) → 멤버 채우기 를 setter 함수 하나로
이전 멤버의 잔재가 남아 새 멤버 일부가 오염된다
4
태그의 0 은 NONE / UNKNOWN — memset(0) 초기화가 “아무것도 없음” 이 된다
초기화 안 한 구조체가 첫 멤버를 가진 것처럼 보인다
5
switch 에 default: 를 둔다 (범위 밖 태그, info 가 없는 타입)
알 수 없는 태그가 조용히 통과한다
6
멤버 안에 동적 할당(포인터) 이 있으면 free 도 태그로 갈라서 한다
잘못된 멤버의 포인터를 free → 크래시 또는 누수
/* 6번 예: 해제도 태그로 가른다 */void nf_profile_free(nf_profile_t *p){ if (p == NULL) return; switch (p->nf_type) { case NF_TYPE_UPF: SAFE_FREE(p->info.upf.big_list); break; /* 이 멤버가 포인터를 가질 때만 */ default: break; } memset(p, 0x00, sizeof(*p));}
union 의 크기는 가장 큰 멤버를 따른다
upf_info_t 가 수 KB 이고 amf_info_t 가 4바이트여도 nf_profile_t 는 항상 수 KB 를 차지한다.
멤버 크기가 비슷하면 → union 이 이득 (합이 아니라 최댓값이니까)
한 멤버만 유독 크면 → 그 멤버는 union 에서 빼고 포인터(upf_info_t *upf)로 두고, 태그가 UPF 일 때만 할당·해제한다
union 의 정렬은 정렬이 가장 큰 멤버를 따른다
4장의 배치 규칙에서 union 은 중첩 struct 와 같은 취급이다. 8바이트 멤버가 하나라도 있으면 union 전체가 8바이트 그룹이다.
union 을 통째로 memcpy 로 전송하지 말 것
전송 바이트열에는 태그가 가리키는 멤버만 써야 한다. 안 쓰는 멤버 자리의 바이트는 쓰레기이고,
상대와 union 크기도 다를 수 있다. pack 함수에서 switch (tag) 로 갈라 필드를 하나씩 쓴다.
→ 3번 문서
3. 배열 — 고정이냐 동적이냐
3-1. 판단 기준
flowchart TD
Q1{"규격서에 최대 개수가<br/>명시되어 있나?"}
Q1 -->|있다| Q2{"최대 개수 × 크기가<br/>수 KB 이하인가?"}
Q1 -->|없다/매우 큼| D["동적 할당<br/>ptr + cnt"]
Q2 -->|예| F["고정 배열<br/>★ 기본 선택"]
Q2 -->|아니오| D
웬만하면 고정 배열
고정 배열은 malloc/free 가 없으니 누수가 원천적으로 불가능하다.
규격서에 “최대 8개” 라고 써 있으면 그냥 arr[8] 로 잡는다.
메모리 몇 KB 아끼려다 누수 버그 하나 만드는 게 훨씬 손해다.
3-2. 고정 배열 (권장)
#define MAX_IPV4_ADDR 8#define IPV4_STR_SIZE 16 /* "255.255.255.255" + '\0' */typedef struct { uint16_t ipv4_cnt; /* ★ 실제 개수 */ char ipv4_addr[MAX_IPV4_ADDR][IPV4_STR_SIZE]; /* ★ 최대 크기 */} nf_profile_t;
/* 추가하는 함수 — 개수 검사를 반드시 넣는다 */int profile_add_ipv4(nf_profile_t *p, const char *addr){ CHK_PTR(p); CHK_STR(addr); if (p->ipv4_cnt >= MAX_IPV4_ADDR) { /* ★ 이게 없으면 오버플로 */ LOG_ERR("ipv4 list full (%u) (%s/%d)", p->ipv4_cnt, __func__, __LINE__); return RET_TOO_SMALL; } if (strlen(addr) >= IPV4_STR_SIZE) { /* ★ 길이도 검사 */ LOG_ERR("addr too long (%zu) (%s/%d)", strlen(addr), __func__, __LINE__); return RET_TOO_SMALL; } snprintf(p->ipv4_addr[p->ipv4_cnt], IPV4_STR_SIZE, "%s", addr); p->ipv4_cnt++; return RET_OK;}
cnt 검사를 빼먹으면 스택이 통째로 깨진다
구조체가 스택에 있으면 배열 뒤에는 리턴 주소가 있다.
cnt 검사 없는 쓰기 한 번이 임의 코드 실행 취약점이 된다.
배열에 쓰는 코드는 무조건 cnt >= MAX 검사부터.
3-3. 동적 배열 (개수를 모를 때만)
typedef struct { uint16_t item_cnt; item_t *items; /* ★ 짝: free */} big_list_t;int list_init(big_list_t *l, uint16_t cnt){ CHK_PTR(l); memset(l, 0x00, sizeof(*l)); if (cnt == 0) return RET_OK; /* 0개는 정상. 할당 안 함 */ l->items = calloc(cnt, sizeof(item_t)); /* ★ 곱셈 오버플로 검사 포함 */ CHK_PTR(l->items); l->item_cnt = cnt; return RET_OK;}void list_free(big_list_t *l){ if (l == NULL) return; SAFE_FREE(l->items); l->item_cnt = 0; /* ★ 개수도 같이 0으로 */}
free 하고 cnt 를 안 지우면 더 위험하다
포인터는 NULL 인데 cnt 가 5로 남아 있으면,
for (i = 0; i < l->item_cnt; i++) l->items[i]... 가 NULL 역참조로 죽는다.
포인터와 개수는 항상 같이 움직인다.
3-4. 문자열 배열 (가장 실수 잦음)
typedef struct { uint16_t cnt; char **strs; /* 포인터의 배열 — 이중 해제 필요 */} str_list_t;void str_list_free(str_list_t *l){ uint16_t i; if (l == NULL) return; if (l->strs != NULL) { for (i = 0; i < l->cnt; i++) { SAFE_FREE(l->strs[i]); /* ① 안쪽 문자열들 먼저 */ } SAFE_FREE(l->strs); /* ② 포인터 배열 나중 */ } l->cnt = 0;}
바깥만 free 하면 문자열 전부가 샌다
free(l->strs) 만 하면 포인터 배열만 없어지고, 각 문자열은 주소를 잃은 채 남는다.
되찾을 방법이 없으니 프로세스가 죽을 때까지 누수다.
안쪽부터 바깥으로 — 해제 순서의 기본 원칙.
→ [C] 개수 필드 + 포인터의 포인터
4. 정렬(Align)과 패딩 — 멤버 순서가 구조체 크기를 정한다
한 문장 요약
컴파일러는 각 멤버를 “자기 크기의 배수 주소”에 놓는다. 안 맞으면 빈 바이트(패딩)를 끼운다.
그래서 정렬 요구가 큰 멤버(8 → 4 → 2 → 1)부터 선언하면 패딩이 거의 사라진다.
4-1. 왜 맞춰야 하나 — CPU 가 읽는 단위
CPU 는 메모리를 바이트 단위가 아니라 워드 / 캐시라인 덩어리로 가져온다.
4바이트 값이 덩어리 경계를 걸쳐 있으면 한 번에 못 읽는다.
패딩을 없앤 구조체의 uint32_t 멤버는 홀수 번지에 놓일 수 있다.
x86 에서는 느려지는 정도지만 ARM 등에서는 SIGBUS 다.
게다가 &p->field 로 포인터를 만들어 넘기면 컴파일러가 정렬돼 있다고 가정한 코드를 만든다.
그래서 네트워크 바이트열은 pack 함수로 한 필드씩 복사하는 쪽이 안전하다.
4-7. 앞 장의 습관과 충돌하는 곳 — 어떻게 풀까
이 문서 앞부분은 “Optional 값 바로 옆에 has_ 플래그” 와 “배열 cnt 를 바로 위에” 를 권했다.
이걸 그대로 지키면 uint8_t 와 uint16_t 가 번갈아 나와 패딩이 생긴다.
방법
내용
평가
(A) has_ 를 한 그룹으로 몰기
값들은 크기순으로, uint8_t has_* 는 1B 그룹에 모아서 배치. 그룹 주석으로 관계를 남김
★★★ 플래그 수가 적을 때
(B) 비트마스크 present 를 맨 위에
uint32_t present 하나가 4B 그룹에 들어가므로 패딩 0
★★★ Optional 이 많을 때 (위 2-3)
(C) 옆에 두고 패딩 감수
가독성 우선
구조체가 몇 개 안 될 때만
/* (A) 예 — 값은 큰 순서, 플래그는 1B 그룹으로 모은다 */typedef struct { uint16_t heart_beat_timer; /* has_heart_beat_timer 와 짝 */ uint16_t priority; /* has_priority 와 짝 */ uint16_t ipv4_cnt; /* ipv4_addr 의 개수 */ /* ── 1B 그룹: Optional 존재 표시 ── */ uint8_t has_heart_beat_timer; uint8_t has_priority; char ipv4_addr[MAX_IPV4_ADDR][IPV4_STR_SIZE]; /* 정렬 1 → 맨 끝 */} nf_profile_t;
cnt 는 "바로 위"가 원칙이지만, char 배열이라면 정렬 문제가 없다
uint16_t cnt 바로 뒤에 char arr[N][M] 이 와도 char 는 정렬 1 이라 패딩이 안 생긴다.
정렬 문제는 cnt 뒤에 uint32_t/uint64_t 원소 배열이 올 때만이다.
그 경우에만 cnt 를 다른 2B 멤버 옆으로 옮긴다.
5. 완성된 복붙 골격
5-1. 헤더 ({{모듈}}_type.h)
/* ══════════════ {{모듈}}_type.h ══════════════ */#ifndef {{모듈대문자}}_TYPE_H#define {{모듈대문자}}_TYPE_H#include <stdint.h>#include "common.h"/* ── ① 길이 상수: 규격서 숫자를 그대로 ─────────────── */#define {{필드}}_LEN {{규격서길이}}#define {{필드}}_SIZE ({{필드}}_LEN + 1)#define MAX_{{배열}}_CNT {{규격서최대개수}}/* ── ② enum: UNKNOWN=0, 끝에 _END ──────────────────── */typedef enum { {{ENUM}}_UNKNOWN = 0, {{ENUM}}_{{값1}}, {{ENUM}}_{{값2}}, {{ENUM}}_END} {{enum타입}}_t;const char *{{enum타입}}_to_str({{enum타입}}_t v);{{enum타입}}_t {{enum타입}}_from_str(const char *s);/* ── ③ 입력 구조체 ─────────────────────────────────── *//* ★ 아래 순서는 '읽기 쉬운 순서'다. 다 짠 뒤 4장 규칙(정렬 큰 것 → 작은 것, char[] 는 맨 끝)으로 재배치하고 * sizeof / offsetof 를 찍어 확인한다. (전송용이면 순서를 바꿔도 pack 함수가 책임진다) */typedef struct { /* Mandatory */ char {{필수문자열}}[{{필드}}_SIZE]; {{enum타입}}_t {{필수enum}}; uint32_t {{필수정수}}; /* Optional — has_ 플래그와 값을 나란히 */ uint8_t has_{{선택필드}}; uint16_t {{선택필드}}; /* 배열 — cnt 를 바로 위에 */ uint16_t {{배열}}_cnt; {{항목타입}} {{배열}}[MAX_{{배열}}_CNT]; /* Conditional ① 조건이 하나뿐 — 구조는 Optional 과 같고 validate 에서 조건을 본다 */ uint8_t has_{{조건필드}}; {{조건타입}} {{조건필드}}; /* Conditional ② 배타 (정확히 하나만) — 태그 + union. 동시에 올 수 있으면 쓰지 말 것 */ union { {{타입A}}_info_t {{a}}; /* 태그 == {{TAG_A}} 일 때만 유효 */ {{타입B}}_info_t {{b}}; /* 여러 값을 갖는 쪽은 배열·cnt 를 자기 struct 안에 */ } info; /* ★ 태그는 위쪽 {{필수enum}} 을 재사용하거나 kind 를 새로 둔다 */} {{모듈}}_req_t;/* ── ④ 출력 구조체 ─────────────────────────────────── */typedef struct { uint16_t result_code; char {{출력필드}}[{{출력}}_SIZE];} {{모듈}}_rsp_t;/* ── ⑤ 생명주기 함수 (자원을 쓰면 반드시 짝으로) ───── */void {{모듈}}_req_init({{모듈}}_req_t *r);int {{모듈}}_req_validate(const {{모듈}}_req_t *r);void {{모듈}}_req_free({{모듈}}_req_t *r); /* 동적 배열이 있을 때만 *//* ── union 이 있으면 반드시 짝으로 (직접 접근 금지) ─── */const {{타입A}}_info_t *{{모듈}}_get_{{a}}(const {{모듈}}_req_t *r); /* 태그 다르면 NULL */int {{모듈}}_set_{{a}}({{모듈}}_req_t *r, const {{타입A}}_info_t *src);/* ── ⑥ Optional 조작 매크로 ────────────────────────── */#define SET_OPT(_s, _f, _v) do { (_s)->has_##_f = 1; (_s)->_f = (_v); } while (0)#define GET_OPT(_s, _f, _def) ((_s)->has_##_f ? (_s)->_f : (_def))#define CLR_OPT(_s, _f) do { (_s)->has_##_f = 0; (_s)->_f = 0; } while (0)#endif /* {{모듈대문자}}_TYPE_H */
5-2. 초기화 · 검증 ({{모듈}}_type.c)
/* ══════════════ {{모듈}}_type.c ══════════════ */#include "{{모듈}}_type.h"#include "guard.h"void {{모듈}}_req_init({{모듈}}_req_t *r){ if (r == NULL) return; memset(r, 0x00, sizeof(*r)); /* ★ memset(0) 만으로 끝나게 타입을 설계했다면 여기 추가 코드가 없다. * 이게 좋은 설계다. enum 0 = UNKNOWN, 문자열 = "", cnt = 0, has_ = 0 */}/* ★ 규격서의 "M/O" 와 "범위" 열이 그대로 코드가 된다 */int {{모듈}}_req_validate(const {{모듈}}_req_t *r){ uint16_t i; CHK_PTR(r); /* ── Mandatory 검사 ── */ if (r->{{필수문자열}}[0] == '\0') { LOG_ERR("{{필수문자열}} is missing (%s/%d)", __func__, __LINE__); return RET_INVALID_ARG; } if (r->{{필수enum}} == {{ENUM}}_UNKNOWN || r->{{필수enum}} >= {{ENUM}}_END) { LOG_ERR("{{필수enum}}(%d) invalid (%s/%d)", r->{{필수enum}}, __func__, __LINE__); return RET_INVALID_ARG; } /* ── 길이 검사 ── */ if (strlen(r->{{필수문자열}}) > {{필드}}_LEN) { LOG_ERR("{{필수문자열}} too long(%zu>%d) (%s/%d)", strlen(r->{{필수문자열}}), {{필드}}_LEN, __func__, __LINE__); return RET_BAD_FORMAT; } /* ── Optional 은 "있을 때만" 범위 검사 ── */ if (r->has_{{선택필드}}) { CHK_RANGE(r->{{선택필드}}, {{최소}}, {{최대}}); } /* ── Conditional ①: 조건이 참일 때만 필수 ── */ if ({{조건식}}) { if (!r->has_{{조건필드}}) { LOG_ERR("{{조건필드}} required when {{조건설명}} (%s/%d)", __func__, __LINE__); return RET_INVALID_ARG; } /* 여기서 범위 검사 */ } else if (r->has_{{조건필드}}) { LOG_WRN("{{조건필드}} ignored (%s/%d)", __func__, __LINE__); /* 또는 거절 — 정책 */ } /* ── Conditional ②: union 은 태그가 가리키는 멤버만 ── */ switch (r->{{필수enum}}) { case {{TAG_A}}: /* r->info.{{a}} 만 검사 */ break; case {{TAG_B}}: /* r->info.{{b}} 만 검사 */ break; default: break; /* ★ default 를 빼지 말 것 */ } /* ── 배열: 개수 + 각 항목 ── */ if (r->{{배열}}_cnt > MAX_{{배열}}_CNT) { LOG_ERR("{{배열}}_cnt(%u) exceeds max(%d) (%s/%d)", r->{{배열}}_cnt, MAX_{{배열}}_CNT, __func__, __LINE__); return RET_INVALID_ARG; } for (i = 0; i < r->{{배열}}_cnt; i++) { /* 각 항목 검사 */ } return RET_OK;}void {{모듈}}_req_free({{모듈}}_req_t *r){ if (r == NULL) return; /* 동적 할당한 멤버가 있으면 여기서 — 안쪽부터 바깥으로 */ memset(r, 0x00, sizeof(*r));}
validate 를 입구에서 딱 한 번 부른다
모든 함수가 모든 필드를 검사하면 코드가 검사문 천지가 된다.
외부 입력이 들어오는 지점에서 validate 한 번, 그 뒤 내부 함수들은 믿는다.
이게 1번 문서의 “경계에서만 빡세게” 원칙의 실천판이다.